/*****************************************************************************
 * Copyright (C) Stacy Curl. All rights reserved.                            *
 * ------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the BSD      *
 * style license a copy of which has been included with this distribution in *
 * the LICENSE.txt file.                                                     *
 *****************************************************************************/
package picounit.features.classloader;

import picounit.PicoUnit;
import picounit.features.base.ClassPath;
import picounit.features.classloader.mutator.ClassMutator;
import picounit.features.classloader.observer.ClassObserver;
import picounit.features.test_finder.ObjectReloader.Deserializer;

import java.util.HashMap;
import java.util.Map;

public class PicoUnitClassLoader extends ClassLoader implements ByteCodeClassLoader {
	private static final String PICOUNIT_PACKAGE = new PackageUtil().packageName(PicoUnit.class);

	private final Map<String, Class> loadedClasses = new HashMap<String, Class>();

	private final ClassObserver classObserver;
	private final ClassMutator classMutator;
	private final ClassPath classPath;

	private final String picoUnitName;

	public PicoUnitClassLoader(ClassLoader parent, ClassObserver classObserver,
		ClassMutator classMutator, ClassPath classPath, String picoUnitName) {

		super(parent);

		this.classObserver = classObserver;
		this.classMutator = classMutator;
		this.classPath = classPath;
		this.picoUnitName = picoUnitName;

		classObserver.setClassLoader(this);
	}

	public boolean loadedClass(String className) {
		return loadedClasses.containsKey(className);
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// ByteCodeClassLoader methods
	///////////////////////////////////////////////////////////////////////////////////////////////

	public Class<?> defineClass(String className, byte[] byteCode) throws ClassFormatError {
		return defineClass(className, byteCode, 0, byteCode.length);
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// ClassLoader methods
	///////////////////////////////////////////////////////////////////////////////////////////////

	@Override
	public Class<?> loadClass(String className) throws ClassNotFoundException {
		if (neededByPicoUnit(className)) {
			return loadClassUsingParentClassLoader(className);
		}

		Class<?> loadedClass = loadedClasses.get(className);

		if (!loadedClass(className)) {
			loadedClass = loadClass(className, classPath.findClass(className));

			loadedClasses.put(className, loadedClass);
		}

		return loadedClass;
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Object methods
	///////////////////////////////////////////////////////////////////////////////////////////////
	
	@Override
	public String toString() {
		return super.toString() + "[" + picoUnitName + "]";
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Implementation
	///////////////////////////////////////////////////////////////////////////////////////////////

	private boolean neededByPicoUnit(String className) {
		return isPartOfPicoUnit(className) || 
			className.startsWith("junit") ||
			className.startsWith("org.picocontainer") ||
			className.startsWith("net.sf.cglib") ||
			className.startsWith("org.jmock") ||
			className.startsWith("moved.org.objectweb.asm") ||
			className.startsWith("java") ||
			className.startsWith("sun");
	}

	private boolean isPartOfPicoUnit(String className) {
		return className.startsWith(PICOUNIT_PACKAGE) &&
			!isDeserializer(className);
	}

	private boolean isDeserializer(String className) {
		return Deserializer.class.getName().equals(className);
	}

	private Class<?> loadClass(String className, byte[] byteCode) throws ClassFormatError,
		ClassNotFoundException {

//		System.out.println(String.format("Loading: %s, with %s", className, this));
		classObserver.observe(byteCode);

		if (classMutator.shouldMutate(className)) {
			return defineClass(className, classMutator.mutate(className, byteCode));
		}
		else if (forceLoad(className)) {
			return defineClass(className, byteCode);
		}
		else {
			return loadClassUsingParentClassLoader(className);
		}
	}

	private boolean forceLoad(String className) {
		return isDeserializer(className);
	}

	private Class<?> loadClassUsingParentClassLoader(String className) throws ClassNotFoundException {
		return getParent().loadClass(className);
	}
}
